using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using StardewModdingAPI.Web.Framework.LogParsing.Models;

namespace StardewModdingAPI.Web.Framework.LogParsing;

/// <summary>Parses SMAPI log files.</summary>
public class LogParser
{
    /*********
    ** Fields
    *********/
    /// <summary>A regex pattern matching the start of a SMAPI message.</summary>
    private readonly Regex MessageHeaderPattern = new(@"^\[(?<time>\d\d[:\.]\d\d[:\.]\d\d) (?<level>[a-z]+)(?: +screen_(?<screen>\d+))? +(?<modName>[^\]]+)\] ", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching SMAPI's initial platform info message.</summary>
    private readonly Regex InfoLinePattern = new(@"^SMAPI (?<apiVersion>.+) with Stardew Valley (?<gameVersion>.+) on (?<os>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching SMAPI's mod folder path line.</summary>
    private readonly Regex ModPathPattern = new(@"^Mods go here: (?<path>.+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching SMAPI's mod folder path line.</summary>
    private readonly Regex GamePathPattern = new(@"^\(Using custom --mods-path argument\. Game folder: (?<path>.+)\.\)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching SMAPI's log timestamp line.</summary>
    private readonly Regex LogStartedAtPattern = new(@"^Log started at (?<timestamp>.+) UTC", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching the start of SMAPI's mod list.</summary>
    private readonly Regex ModListStartPattern = new(@"^Loaded \d+ mods:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching an entry in SMAPI's mod list.</summary>
    /// <remarks>The author name and description are optional.</remarks>
    private readonly Regex ModListEntryPattern = new(@"^   (?<name>.+?) (?<version>[^\s]+)(?: by (?<author>[^\|]+))?(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching the start of SMAPI's content pack list.</summary>
    private readonly Regex ContentPackListStartPattern = new(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching an entry in SMAPI's content pack list.</summary>
    private readonly Regex ContentPackListEntryPattern = new(@"^   (?<name>.+?) (?<version>[^\s]+)(?: by (?<author>[^\|]+))? \| for (?<for>[^\|]*)(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching the start of SMAPI's skipped mods list.</summary>
    private readonly Regex SkippedModListStartPattern = new("^   Skipped mods$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching an entry in SMAPI's skipped mods list.</summary>
    private readonly Regex SkippedModListEntryPattern = new(@"^   (?:--------------------------------------------------|   These mods could not be added to your game\.|   - (?<name>.+?) (?<version>[^\s]+)(?: because .+)?)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching the start of SMAPI's mod update list.</summary>
    private readonly Regex ModUpdateListStartPattern = new(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching an entry in SMAPI's mod update list.</summary>
    private readonly Regex ModUpdateListEntryPattern = new(@"^   (?<name>.+) (?<newVersion>[^\s]+): (?<link>[^\s]+)(?: \(you have (?<oldVersion>[^\)]+)\))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);

    /// <summary>A regex pattern matching SMAPI's update line.</summary>
    private readonly Regex SmapiUpdatePattern = new(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);


    /*********
    ** Public methods
    *********/
    /// <summary>Parse SMAPI log text.</summary>
    /// <param name="logText">The SMAPI log text.</param>
    public ParsedLog Parse(string? logText)
    {
        try
        {
            // skip if empty
            if (string.IsNullOrWhiteSpace(logText))
            {
                return new ParsedLog
                {
                    IsValid = false,
                    RawText = logText,
                    Error = "The log is empty."
                };
            }

            // init log
            ParsedLog log = new()
            {
                IsValid = true,
                RawText = logText,
                Messages = this.CollapseRepeats(this.GetMessages(logText)).ToArray()
            };

            // parse log messages
            LogModInfo smapiMod = new(ModType.Special, name: "SMAPI", author: "Pathoschild", version: "", description: "", loaded: true);
            LogModInfo gameMod = new(ModType.Special, name: "game", author: "", version: "", description: "", loaded: true);
            IDictionary<string, List<LogModInfo>> mods = new Dictionary<string, List<LogModInfo>>();
            bool inModList = false;
            bool inContentPackList = false;
            bool inSkippedModsList = false;
            bool inModUpdateList = false;
            foreach (LogMessage message in log.Messages)
            {
                // collect stats
                if (message.Level == LogLevel.Error)
                {
                    switch (message.Mod)
                    {
                        case "SMAPI":
                            smapiMod.Errors++;
                            break;

                        case "game":
                            gameMod.Errors++;
                            break;

                        default:
                            if (mods.TryGetValue(message.Mod, out var entries))
                            {
                                foreach (LogModInfo entry in entries)
                                    entry.Errors++;
                            }
                            break;
                    }
                }

                // detect split-screen mode
                if (message.ScreenId != 0)
                    log.IsSplitScreen = true;

                // collect SMAPI metadata
                if (message.Mod == "SMAPI")
                {
                    // update flags
                    inModList = inModList && message.Level == LogLevel.Info && this.ModListEntryPattern.IsMatch(message.Text);
                    inContentPackList = inContentPackList && message.Level == LogLevel.Info && this.ContentPackListEntryPattern.IsMatch(message.Text);
                    inSkippedModsList = inSkippedModsList && message.Level == LogLevel.Error && this.SkippedModListEntryPattern.IsMatch(message.Text);
                    inModUpdateList = inModUpdateList && message.Level == LogLevel.Alert && this.ModUpdateListEntryPattern.IsMatch(message.Text);

                    // mod list
                    if (!inModList && message.Level == LogLevel.Info && this.ModListStartPattern.IsMatch(message.Text))
                    {
                        inModList = true;
                        message.IsStartOfSection = true;
                        message.Section = LogSection.ModsList;
                    }
                    else if (inModList)
                    {
                        Match match = this.ModListEntryPattern.Match(message.Text);
                        string name = match.Groups["name"].Value;
                        string version = match.Groups["version"].Value;
                        string author = match.Groups["author"].Value;
                        string description = match.Groups["description"].Value;

                        if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
                            mods[name] = entries = [];
                        entries.Add(new LogModInfo(ModType.CodeMod, name: name, author: author, version: version, description: description, loaded: true));

                        message.Section = LogSection.ModsList;
                    }

                    // content pack list
                    else if (!inContentPackList && message.Level == LogLevel.Info && this.ContentPackListStartPattern.IsMatch(message.Text))
                    {
                        inContentPackList = true;
                        message.IsStartOfSection = true;
                        message.Section = LogSection.ContentPackList;
                    }
                    else if (inContentPackList)
                    {
                        Match match = this.ContentPackListEntryPattern.Match(message.Text);
                        string name = match.Groups["name"].Value;
                        string version = match.Groups["version"].Value;
                        string author = match.Groups["author"].Value;
                        string description = match.Groups["description"].Value;
                        string forMod = match.Groups["for"].Value.Trim(); // if there's no mod description, trim newline from ID

                        if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
                            mods[name] = entries = [];
                        entries.Add(new LogModInfo(ModType.ContentPack, name: name, author: author, version: version, description: description, contentPackFor: forMod, loaded: true));

                        message.Section = LogSection.ContentPackList;
                    }

                    // skipped mods list
                    if (!inSkippedModsList && message.Level == LogLevel.Error && this.SkippedModListStartPattern.IsMatch(message.Text))
                        inSkippedModsList = true;
                    else if (inSkippedModsList)
                    {
                        Match match = this.SkippedModListEntryPattern.Match(message.Text);

                        if (match.Groups["name"].Success)
                        {
                            string name = match.Groups["name"].Value;
                            string version = match.Groups["version"].Value;

                            if (!mods.TryGetValue(name, out List<LogModInfo>? entries))
                                mods[name] = entries = [];
                            entries.Add(new LogModInfo(ModType.Unknown, name: name, author: "", version: version, description: "", contentPackFor: null, loaded: false));
                        }
                    }

                    // mod update list
                    else if (!inModUpdateList && message.Level == LogLevel.Alert && this.ModUpdateListStartPattern.IsMatch(message.Text))
                    {
                        inModUpdateList = true;
                        message.IsStartOfSection = true;
                        message.Section = LogSection.ModUpdateList;
                    }
                    else if (inModUpdateList)
                    {
                        Match match = this.ModUpdateListEntryPattern.Match(message.Text);
                        string name = match.Groups["name"].Value;
                        string oldVersion = match.Groups["oldVersion"].Value;
                        string newVersion = match.Groups["newVersion"].Value;
                        string link = match.Groups["link"].Value;

                        if (!mods.TryGetValue(name, out var entries))
                            mods[name] = entries = [new LogModInfo(ModType.Unknown, name: name, author: "", version: oldVersion, description: "", loaded: false)];

                        foreach (LogModInfo entry in entries)
                            entry.SetUpdate(newVersion, link);

                        message.Section = LogSection.ModUpdateList;
                    }
                    else if (message.Level == LogLevel.Alert && this.SmapiUpdatePattern.IsMatch(message.Text))
                    {
                        Match match = this.SmapiUpdatePattern.Match(message.Text);
                        string version = match.Groups["version"].Value;
                        string link = match.Groups["link"].Value;

                        smapiMod.SetUpdate(version, link);
                    }

                    // platform info line
                    else if (message.Level == LogLevel.Info && this.InfoLinePattern.IsMatch(message.Text))
                    {
                        Match match = this.InfoLinePattern.Match(message.Text);
                        log.ApiVersion = match.Groups["apiVersion"].Value;
                        log.GameVersion = match.Groups["gameVersion"].Value;
                        log.OperatingSystem = match.Groups["os"].Value;

                        const string strictModeSuffix = " (strict mode)";
                        if (log.ApiVersion.EndsWith(strictModeSuffix))
                        {
                            log.IsStrictMode = true;
                            log.ApiVersion = log.ApiVersion[..^strictModeSuffix.Length];
                        }

                        smapiMod.OverrideVersion(log.ApiVersion);
                        log.ApiVersionParsed = smapiMod.GetParsedVersion();
                    }

                    // mod path line
                    else if (message.Level == LogLevel.Info && this.ModPathPattern.IsMatch(message.Text))
                    {
                        Match match = this.ModPathPattern.Match(message.Text);
                        log.ModPath = match.Groups["path"].Value;
                        int lastDelimiterPos = log.ModPath.LastIndexOfAny(['/', '\\']);
                        log.GamePath = lastDelimiterPos >= 0
                            ? log.ModPath[..lastDelimiterPos]
                            : log.ModPath;
                    }

                    // game path line
                    else if (message.Level == LogLevel.Trace && this.GamePathPattern.IsMatch(message.Text))
                    {
                        Match match = this.GamePathPattern.Match(message.Text);
                        log.GamePath = match.Groups["path"].Value;
                    }

                    // log UTC timestamp line
                    else if (message.Level == LogLevel.Trace && this.LogStartedAtPattern.IsMatch(message.Text))
                    {
                        Match match = this.LogStartedAtPattern.Match(message.Text);
                        log.Timestamp = DateTime.Parse(match.Groups["timestamp"].Value + "Z");
                    }
                }
            }

            // finalize log
            if (log.GameVersion != null)
                gameMod.OverrideVersion(log.GameVersion);
            log.Mods = new[] { gameMod, smapiMod }.Concat(mods.Values.SelectMany(p => p).OrderBy(p => p.Name)).ToArray();
            return log;
        }
        catch (LogParseException ex)
        {
            return new ParsedLog
            {
                IsValid = false,
                Error = ex.Message,
                RawText = logText
            };
        }
        catch (Exception ex)
        {
            return new ParsedLog
            {
                IsValid = false,
                Error = $"Parsing the log file failed. Technical details:\n{ex}",
                RawText = logText
            };
        }
    }


    /*********
    ** Private methods
    *********/
    /// <summary>Collapse consecutive repeats into the previous message.</summary>
    /// <param name="messages">The messages to filter.</param>
    private IEnumerable<LogMessage> CollapseRepeats(IEnumerable<LogMessage> messages)
    {
        LogMessage? next = null;

        foreach (LogMessage message in messages)
        {
            // new message
            if (next == null)
            {
                next = message;
                continue;
            }

            // repeat
            if (next.Level == message.Level && next.Mod == message.Mod && next.Text == message.Text)
            {
                next.Repeated++;
                continue;
            }

            // non-repeat message
            yield return next;
            next = message;
        }

        if (next != null)
            yield return next;
    }

    /// <summary>Split a SMAPI log into individual log messages.</summary>
    /// <param name="logText">The SMAPI log text.</param>
    /// <exception cref="LogParseException">The log text can't be parsed successfully.</exception>
    private IEnumerable<LogMessage> GetMessages(string logText)
    {
        LogMessageBuilder builder = new();
        using StringReader reader = new(logText);
        while (true)
        {
            // read line
            string? line = reader.ReadLine();
            if (line == null)
                break;

            // match header
            Match header = this.MessageHeaderPattern.Match(line);
            bool isNewMessage = header.Success;

            // start/continue message
            if (isNewMessage)
            {
                if (builder.Started)
                {
                    yield return builder.Build()!;
                    builder.Clear();
                }

                Group screenGroup = header.Groups["screen"];
                builder.Start(
                    time: header.Groups["time"].Value,
                    level: Enum.Parse<LogLevel>(header.Groups["level"].Value, ignoreCase: true),
                    screenId: screenGroup.Success ? int.Parse(screenGroup.Value) : 0, // main player is always screen ID 0
                    mod: header.Groups["modName"].Value,
                    text: line[header.Length..]
                );
            }
            else
            {
                if (!builder.Started)
                    throw new LogParseException("Found a log message with no SMAPI metadata. Is this a SMAPI log file?");

                builder.AddLine(line);
            }
        }

        // end last message
        if (builder.Started)
            yield return builder.Build()!;
    }
}
